//
//  PMBody.m
//  PezMonkey
//
//  Created by Spence DiNicolantonio on 10/28/09.
//  Copyright 2009 Necrosoft. All rights reserved.
//

/**
 * A rigid body. PMBody objects describe and maintain a physical state in the 
 * physics space. The state (position, velocity, etc.) of a PMBody within a PMSpace 
 * will be affected by gravity. PMBody objects do not, however, handle 
 * any kind of collision. In order to have PMBody states altered by collisions,
 * you must use the PMBody with a corresponding PMShape object. For more information, 
 * see the PMShape class. PMBodys created for use exclusively with static PMShapes
 * should be initialized with mass and moment of inertia set to infinity. This
 * can be easily accomplished by using the staticBody convenience constructor.
 */

#import "PMBody.h"


@implementation PMBody

@synthesize data;
@synthesize delegate;


#pragma mark -
#pragma mark Init

/**
 * Returns a PMBody object initialized with infinite mass and moment of inertia. 
 * This method should be used to initialize PMBodys to be used exclusively with 
 * static PMShapes.
 */
- (id)init {
	return [self initWithMass:PMInfinity 
					   moment:PMInfinity];
}

/**
 * Returns a PMBody object initialized with a given mass and moment of inertia.
 */
- (id)initWithMass:(PMFloat)mass moment:(PMFloat)moment {
	if (![super init])
		return nil;
	
	// allocate and initialize cpBody
	body = cpBodyNew(mass, moment);
	
	// reference this PMBody object with cpBody's data pointer
	// this is for internal use
	body->data = self;
	
	// set cpBody's position and velocity functions
	body->velocity_func = (cpBodyVelocityFunc)PMBodyChipmunkVelocityFunction;
	body->position_func = (cpBodyPositionFunc)PMBodyChipmunkPositionFunction;
	
	// set delegate to nil
	self.delegate = nil;
	
	return self;
}


#pragma mark Convenience

/**
 * Creates and returns a PMBody object initialized with infinite mass and moment 
 * of inertia. This constructor should be used to create PMBody objects for use
 * with static collision shapes.
 */
+ (PMBody *)staticBody {
	return [[[PMBody alloc] init] autorelease];
}

/**
 * Creates and returns a PMBody object initialized with a given mass and moment
 * of inertia.
 */
+ (PMBody *)bodyWithMass:(PMFloat)mass moment:(PMFloat)moment {
	return [[[PMBody alloc] initWithMass:mass 
								  moment:moment] autorelease];
}


#pragma mark -
#pragma mark Dealloc

/**
 * Releases member objects and frees memory used for cpBody before deallocating.
 */
- (void)dealloc {
	[self.data release];
	cpBodyFree(body);
	[super dealloc];
}


#pragma mark -
#pragma mark Getters

/**
 * Returns the receiver's cpBody reference. This is not intended for external use.
 */
- (cpBody *)cpBody {
	return body;
}

/**
 * Returns the receiver's mass.
 */
- (PMFloat)mass {
	return cpBodyGetMass(body);
}

/**
 * Returns the inverse of the receiver's mass.
 */
- (PMFloat)massInverse {
	return body->m_inv;
}

/**
 * Returns the receiver's moment of iniertia.
 */
- (PMFloat)moment {
	return cpBodyGetMoment(body);
}

/**
 * Returns the inverse of the receiver's moment of inertia.
 */
- (PMFloat)momentInverse {
	return body->i_inv;
}

/**
 * Returns the receiver's position vector.
 */
- (PMVect)position {
	return cpBodyGetPos(body);
}

/**
 * Returns the receiver's velocity vector.
 */
- (PMVect)velocity {
	return cpBodyGetVel(body);
}

/**
 * Returns the receiver's terminal velocity. The maximum possible magnitude of 
 * the receiver's integrating velocity.
 */
- (PMFloat)terminalVelocity {
	return cpBodyGetVelLimit(body);
}

/**
 * Returns the receiver's net force vector.
 */
- (PMVect)force {
	return cpBodyGetForce(body);
}

/**
 * Returns the receiver's angular position in radians.
 */
- (PMFloat)angle {
	return cpBodyGetAngle(body);
}

/**
 * Returns the receiver's rotation vector. 
 */
- (PMVect)rotationVector {
	return cpBodyGetRot(body);
}

/**
 * Returns the receiver's angular velocity.
 */
- (PMFloat)angularVelocity {
	return cpBodyGetAngVel(body);
}

/**
 * Returns the receiver's terminal velocity. The maximum possible magnitude of 
 * the receiver's integrating velocity.
 */
- (PMFloat)terminalAngularVelocity {
	return cpBodyGetAngVelLimit(body);
}

/**
 * Returns the receiver's net torque.
 */
- (PMFloat)torque {
	return cpBodyGetTorque(body);
}


#pragma mark -
#pragma mark Setters

/**
 * Sets the receiver's mass to a given value.
 */
- (void)setMass:(PMFloat)mass {
	cpBodySetMass(body, mass);
}

/**
 * Sets the receiver's moment of inertia to a given value.
 */
- (void)setMoment:(PMFloat)moment {
	cpBodySetMoment(body, moment);
}

/**
 * Sets the receiver's position to a given vector.
 */
- (void)setPosition:(PMVect)position {
	cpBodySetPos(body, position);
}

/**
 * Sets the receiver's velocity to a given vector.
 */
- (void)setVelocity:(PMVect)velocity {
	cpBodySetVel(body, velocity);
}

/**
 * Sets the magnitude of the receiver's terminal velocity to a given value.
 */
- (void)setTerminalVelocity:(PMFloat)terminalVelocity {
	cpBodySetVelLimit(body, terminalVelocity);
}

/**
 * Sets the receiver's net force to a given vector.
 */
- (void)setForce:(PMVect)force {
	cpBodySetForce(body, force);
}

/**
 * Sets the receiver's angular position to a given radian value.
 */
- (void)setAngle:(PMFloat)angle {
	cpBodySetAngle(body, angle);
}

/**
 * Sets the receiver's angular velocity to a given value.
 */
- (void)setAngularVelocity:(PMFloat)angularVelocity {
	cpBodySetAngVel(body, angularVelocity);
}

/**
 * Sets the magnitude of the receiver's terminal velocity to a given value.
 */
- (void)setTerminalAngularVelocity:(PMFloat)terminalAngularVelocity {
	cpBodySetAngVelLimit(body, terminalAngularVelocity);
}

/**
 * Sets the receiver's net torque to a given value.
 */
- (void)setTorque:(PMFloat)torque {
	cpBodySetTorque(body, torque);
}


#pragma mark -
#pragma mark Force Application

/**
 * Applies a given impulse to the receiver at the center of gravity. Impulse vector
 * should be given in world coordinates.
 */
- (void)applyImpulse:(PMVect)impulse {
	cpBodyApplyImpulse(body, impulse, PMVectZero);
}

/**
 * Applies a given impulse to the receiver at a given offset from the center of
 * gravity. Both vectors should be given in world coordinates.
 */
- (void)applyImpulse:(PMVect)impulse atRelativeOffset:(PMVect)offset {
	cpBodyApplyImpulse(body, impulse, offset);
}

/**
 * Applies (accumulates) a given force to the receiver at the center of gravity.
 * Force vector should be in world coordinates.
 */
- (void)applyForce:(PMVect)force {
	cpBodyApplyForce(body, force, PMVectZero);
}

/**
 * Applies (accumulates) a given force to the receiver at a given offset from 
 * the center of gravity. Both vectors should be given in world coordinates.
 */
- (void)applyForce:(PMVect)force atRelativeOffset:(PMVect)offset {
	cpBodyApplyForce(body, force, offset);
}

/**
 * Applies (accumulates) a given torque to the receiver.
 */
- (void)applyTorque:(PMFloat)torque {
	cpBodySetTorque(body, cpBodyGetTorque(body) + torque);
}

/**
 * Sets net force and torque of the receiver to zero.
 */
- (void)resetForces {
	cpBodyResetForces(body);
}


#pragma mark -
#pragma mark Update

/**
 * Updates the receiver's velocity based on the net force being applied to the 
 * receiver, a given gravity, and a given damping amount. This method can be used 
 * to manually control static bodies that have not been added to a space. However, 
 * this method is called automatically for all bodies contained in a space. Thus,
 * manually updating the velocity of a body that has already been added to a space 
 * can cause unrealistic results. This method will not cause invocation of any 
 * delegate velocity methods.
 */
- (void)updateVelocityWithTime:(NSTimeInterval)time gravity:(PMVect)gravity damping:(PMFloat)damping {
	// convert gravity from Gs
	PMVect cpGravity = PMVectMult(gravity, PMGravityConst);
	
	cpBodyUpdateVelocity(body, cpGravity, damping, (PMFloat)time);
}

/**
 * Updates the receiver's velocity based on the receiver's current velocity. This 
 * method can be used to manually control static bodies that have not been added 
 * to a space. However, this method is called automatically for all bodies contained 
 * in a space. Thus, manually updating the position of a body that has already 
 * been added to a space can cause unrealistic results. This method will not cause 
 * invocation of any delegate position methods.
 */
- (void)updatePositionWithTime:(NSTimeInterval)time {
	cpBodyUpdatePosition(body, (PMFloat)time);
}


#pragma mark -
#pragma mark Other Methods

/**
 * Modifies the receiver's velocity so that it will move to a point in a given
 * amount of time. This method is intended for movement of bodies without regard 
 * to local physics, ie. dragging.
 */
- (void)slewToPoint:(PMVect)point inTime:(NSTimeInterval)time {
	cpBodySlew(body, point, (PMFloat)time);
}

/**
 * Converts a given point from the receiver's local coordinate system to world
 * coordinates.
 */
- (PMVect)convertPointFromLocal:(PMVect)point {
	return (PMVect) cpBodyLocal2World(body, point);
}

/**
 * Converts a given point from world coordinates to the receiver's local coordinate
 * system.
 */
- (PMVect)convertPointToLocal:(PMVect)point {
	return (PMVect) cpBodyWorld2Local(body, point);
}

/**
 * Converts a given point from world coordinates to the receiver's local coordinate
 * system.
 */
- (PMVect)convertPointFromWorld:(PMVect)point {
	return [self convertPointToLocal:point];
}

/**
 * Converts a given point from the receiver's local coordinate system to world
 * coordinates.
 */
- (PMVect)convertPointToWorld:(PMVect)point {
	return [self convertPointFromLocal:point];
}

@end


#pragma mark -
#pragma mark Functions

/**
 * This function is registered in the backend Chipmunk engine as the velocity
 * update function for all PMBody's cpBody reference. This function will simply 
 * call the updateVelocityWithTime:gravity:damping: method. An exception is thrown
 * if the given cpBody does not reference a valid PMBody object.
 */
static void PMBodyChipmunkVelocityFunction(cpBody *bodyRef, cpVect gravity, cpFloat damping, cpFloat dt) {
	// get PMBody instance from cpBody
	PMBody *body = (PMBody *) bodyRef->data;
	
	// not a PMBody?
	if (![body isKindOfClass:PMBody.class]) {
		[NSException raise:@"Invalid cpBody data pointer" 
					format:@"cpBody data pointer not a valid PMBody object (cpBody created explicitly?)"];
		return;
	}
	
	
	BOOL callDefault = YES;
	id delegate = body.delegate;
	
	// check for delegate pre-update method
	if (delegate && [delegate respondsToSelector:@selector(body:willUpdateVelocityWithTime:gravity:damping:)])
		callDefault = [delegate body:body 
		  willUpdateVelocityWithTime:dt
							 gravity:gravity
							 damping:damping];
	
	// perform default velocity update?
	if (callDefault)
		cpBodyUpdateVelocity(body.cpBody, gravity, damping, dt);
	
	// check for delegate post-update method
	if (delegate && [delegate respondsToSelector:@selector(body:didUpdateVelocityWithTime:)])
		[delegate body:body didUpdateVelocityWithTime:dt];
}
	
/**
 * This function is registered in the backend Chipmunk engine as the position
 * update function for all PMBody's cpBody reference. This function will simply 
 * call the updateVelocityWithTime:gravity:damping: method. An exception is thrown
 * if the given cpBody does not reference a valid PMBody object.
 */
static void PMBodyChipmunkPositionFunction(cpBody *bodyRef, cpFloat dt) {
	// get PMBody instance from cpBody
	PMBody *body = (PMBody *) bodyRef->data;
	
	// not a PMBody?
	if (![body isKindOfClass:PMBody.class]) {
		[NSException raise:@"Invalid cpBody data pointer" 
					format:@"cpBody data pointer not a valid PMBody object (cpBody created explicitly?)"];
		return;
	}
	
	
	BOOL callDefault = YES;
	id delegate = body.delegate;
	
	// check for delegate pre-update method
	if (delegate &&	[delegate respondsToSelector:@selector(body:willUpdatePositionWithTime:)])
		callDefault = [delegate body:body willUpdatePositionWithTime:dt];
	
	// perform default velocity update?
	if (callDefault)
		cpBodyUpdatePosition(body.cpBody, dt);
	
	// check for delegate post-update method
	if (delegate && [delegate respondsToSelector:@selector(body:didUpdatePositionWithTime:)])
		[delegate body:body didUpdatePositionWithTime:dt];
}
